import * as PDFJS from 'pdfjs-dist';
import PDFWorker from 'pdfjs-dist/legacy/build/pdf.worker.min?url';
import { isRef, shallowRef, watch } from 'vue';

import type { PDFDocumentLoadingTask, PDFDocumentProxy } from 'pdfjs-dist';
import type { Ref } from 'vue';
import type { OnPasswordCallback, PDFDestination, PDFInfo, PDFOptions, PDFSrc } from './types';
import { getDestinationArray, getDestinationRef, getLocation, isSpecLike } from './utils/destination';
import { addStylesToIframe, createIframe } from './utils/miscellaneous';

// Could not find a way to make this work with vite, importing the worker entry bundle the whole worker to the the final output
// https://erindoyle.dev/using-pdfjs-with-vite/
// PDFJS.GlobalWorkerOptions.workerSrc = PDFWorker
function configWorker(wokerSrc: string) {
	PDFJS.GlobalWorkerOptions.workerSrc = wokerSrc;
}

/**
 * @typedef {Object} UsePDFParameters
 * @property {string} password
 * Document password to unlock content
 * @property {function} onProgress
 * Callback to request a password if a wrong or no password was provided. The callback receives two parameters: a function that should be called with the new password, and a reason (see PasswordResponses).
 * @property {function} onPassword
 * Callback to be able to monitor the loading progress of the PDF file (necessary to implement e.g. a loading bar). The callback receives an OnProgressParameters argument. if this function is used option.password is ignored
 * @property {function} onError
 * Callback to be able to handle errors during loading
 * */

/**
 *
 * @param {string | URL | TypedArray | PDFDataRangeTransport | DocumentInitParameters} src
 * Can be a URL where a PDF file is located, a typed array (Uint8Array) already populated with data, or a parameter object.
 * @param {UsePDFParameters} options
 * UsePDF object parameters
 */
export function usePDF(
	src: PDFSrc | Ref<PDFSrc>,
	options: PDFOptions = {
		onProgress: undefined,
		onPassword: undefined,
		onError: undefined,
		password: '',
	}
) {
	if (!PDFJS.GlobalWorkerOptions?.workerSrc) configWorker(PDFWorker);

	const pdf = shallowRef<PDFDocumentLoadingTask>();
	const pdfDoc = shallowRef<PDFDocumentProxy>();
	const pages = shallowRef(0);
	const info = shallowRef<PDFInfo | {}>({});

	function processLoadingTask(source: PDFSrc) {
		if (pdfDoc.value) void pdfDoc.value.destroy();

		const loadingTask = PDFJS.getDocument(source!);
		if (options.onProgress) loadingTask.onProgress = options.onProgress;

		if (options.onPassword) {
			loadingTask.onPassword = options.onPassword;
		} else if (options.password) {
			const onPassword: OnPasswordCallback = (updatePassword) => {
				updatePassword(options.password ?? '');
			};
			loadingTask.onPassword = onPassword;
		}

		loadingTask.promise.then(
			async (doc) => {
				pdfDoc.value = doc;

				pdf.value = doc.loadingTask;
				pages.value = doc.numPages;

				const metadata = await doc.getMetadata();
				const attachments = (await doc.getAttachments()) as Record<string, unknown>;
				const javascript = await doc.getJSActions();
				const outline = await doc.getOutline();

				info.value = {
					metadata,
					attachments,
					javascript,
					outline,
				};
			},
			(error) => {
				// PDF loading error
				if (typeof options.onError === 'function') options.onError(error);
			}
		);
	}

	async function getPDFDestination(destination: string | any[] | null): Promise<PDFDestination | null> {
		const document = await pdf.value?.promise;
		if (!document) return null;

		const destArray = await getDestinationArray(document, destination);
		const destRef = await getDestinationRef(document, destArray);
		if (!destRef || !destArray) return null;

		const pageIndex = await document.getPageIndex(destRef);

		const name = destArray[1].name;
		const rest = destArray.slice(2);

		const location = isSpecLike(rest) ? getLocation(name, rest) : null;

		return { pageIndex, location: location ?? { type: 'Fit', spec: [] } };
	}

	async function download(filename = 'filename') {
		if (!pdfDoc.value) throw new Error('Current PDFDocumentProxy have not loaded yet');
		const bytes = await pdfDoc.value?.saveDocument();
		const blobBytes = new Blob([bytes], { type: 'application/pdf' });
		const blobUrl = URL.createObjectURL(blobBytes);

		const anchorDownload = document.createElement('a');
		document.body.appendChild(anchorDownload);
		anchorDownload.href = blobUrl;
		anchorDownload.download = filename;
		anchorDownload.style.display = 'none';
		anchorDownload.click();

		setTimeout(() => {
			URL.revokeObjectURL(blobUrl);
			document.body.removeChild(anchorDownload);
		}, 10);
	}

	async function print(dpi = 150, filename = 'filename') {
		if (!pdfDoc.value) throw new Error('Current PDFDocumentProxy have not loaded yet');
		const bytes = await pdfDoc.value?.saveDocument();
		const savedLoadingTask = PDFJS.getDocument(bytes.buffer);
		const savedDocument = await savedLoadingTask.promise;

		const PRINT_UNITS = dpi / 72;
		const CSS_UNITS = 96 / 72;

		const iframe = await createIframe();
		const contentWindow = iframe.contentWindow;
		contentWindow!.document.title = filename;

		const pagesNumbers = [...Array(savedDocument.numPages).keys()].map((val) => val + 1);

		for (const pageNumber of pagesNumbers) {
			const pageToPrint = await savedDocument.getPage(pageNumber);
			const viewport = pageToPrint.getViewport({ scale: 1 })!;

			if (pageNumber === 1) {
				addStylesToIframe(contentWindow!, (viewport.width * PRINT_UNITS) / CSS_UNITS, (viewport.height * PRINT_UNITS) / CSS_UNITS);
			}

			const canvas = document.createElement('canvas');
			canvas.width = viewport.width * PRINT_UNITS;
			canvas.height = viewport.height * PRINT_UNITS;

			const canvasCloned = canvas.cloneNode() as HTMLCanvasElement;
			contentWindow?.document.body.appendChild(canvasCloned);

			await pageToPrint?.render({
				canvasContext: canvas.getContext('2d')!,
				intent: 'print',
				transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0],
				viewport,
			}).promise;

			canvasCloned.getContext('2d')?.drawImage(canvas, 0, 0);
		}

		contentWindow?.focus();
		contentWindow?.print();
		document.body.removeChild(iframe);
	}

	if (isRef(src)) {
		if (src.value) processLoadingTask(src.value);
		watch(src, () => {
			if (src.value) processLoadingTask(src.value);
		});
	} else {
		if (src) processLoadingTask(src);
	}

	return {
		pdf,
		pages,
		info,
		print,
		download,
		getPDFDestination,
	};
}
